﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Drawing;
using System.Collections;
using System.Globalization;

namespace GeoPal
{
    /// <summary>
    /// Class that represents the Mapper module
    /// </summary>
    class Mapper
    {
        /// <summary>
        /// Enumerator that contains the current function of the Map panel
        /// </summary>
        public enum State
        {
            Splash,
            Radar,
            GoogleMap
        }

        /// <summary>
        /// Current image to display on the Map panel
        /// </summary>
        public Image curMap;
        /// <summary>
        /// Current state of the Map panel
        /// </summary>
        public State state;

        private static CultureInfo googleCultureInfo = new CultureInfo("en-US");
        private static int offset = 268435456;
        private static double radius = offset / Math.PI;
        private double radarXNormalizer;
        private double radarYNormalizer;

        private int width, height;
        private double centerLatitude, centerLongitude;
        private int zoomRadar;
        /// <summary>
        /// Current zoom level of the map
        /// </summary>
        public uint zoomLevel;
        /// <summary>
        /// Show messages on the map
        /// </summary>
        public bool showMessages;
        /// <summary>
        /// The string of text shown on the map
        /// </summary>
        public string text;
        /// <summary>
        /// Number of map requests
        /// </summary>
        public int countGoogleRequests = 0;
        private Bitmap noImageBitmap;

        /// <summary>
        /// Delegate to update messages
        /// </summary>
        public UpdateStatusMessageDelegate updateMessageCallback;
        /// <summary>
        /// Delegate to update the map
        /// </summary>
        public MapRefreshDelegate mapRefreshCallback;

        private Brush brushActive = new SolidBrush(Color.Green);
        private Brush brushInactive = new SolidBrush(Color.Red);
        private Brush brushMarker = new SolidBrush(Color.FromArgb(224, 249, 254));
        private Brush brushMarkerFont = new SolidBrush(Color.Black);
        private Color colorMarker = Color.FromArgb(184, 230, 253);
        private Pen penMarker = new Pen(Color.FromArgb(109, 206, 255), 2);
        private Font fontMarker = new Font(FontFamily.GenericSerif, 5, FontStyle.Regular);

        /// <summary>
        /// Constructor of the Mapper class
        /// </summary>
        /// <param name="_width">Width of the Map panel</param>
        /// <param name="_height">Height of the Map panel</param>
        public Mapper(int _width, int _height)
        {
            state = State.Splash;
            width = _width;
            height = _height;
            radarXNormalizer = (width / 2) / Math.Log(201);
            radarYNormalizer = (height / 2) / Math.Log(201);
            zoomLevel = 1;
            zoomRadar = 1;
            text = "";
            System.IO.Stream dataStream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("GeoPal.Resources.no-map.jpg");
            noImageBitmap = new System.Drawing.Bitmap(dataStream);
        }

        /// <summary>
        /// Converts longitude to x coordinate
        /// </summary>
        /// <param name="lon">Decimal longitude</param>
        /// <returns>X coordinate</returns>
        public static int LonToX(double lon)
        {
            return (int)Math.Round(offset + radius * lon * Math.PI / 180);
        }
        /// <summary>
        /// Converts latitude to y coordinate
        /// </summary>
        /// <param name="lat">Decimal latitude</param>
        /// <returns>Y coordinate</returns>
        public static int LatToY(double lat)
        {
            return (int)Math.Round(offset - radius * Math.Log((1 + Math.Sin(lat * Math.PI / 180)) / (1 - Math.Sin(lat * Math.PI / 180))) / 2);
        }
        /// <summary>
        /// Converts x coordinate to longitude
        /// </summary>
        /// <param name="x">X coordinate</param>
        /// <returns>Decimal longitude</returns>
        public static double XToLon(int x)
        {
            return ((x - offset) / radius) * 180 / Math.PI;
        }
        /// <summary>
        /// Converts y coordinate to latitude
        /// </summary>
        /// <param name="y">Y coordinate</param>
        /// <returns>Decimal latitude</returns>
        public static double YToLat(int y)
        {
            return (Math.PI / 2 - 2 * Math.Atan(Math.Exp((y - offset) / radius))) * 180 / Math.PI;
        }
        /// <summary>
        /// Converts a geographical position into screen coords
        /// </summary>
        /// <param name="lat">Latitude</param>
        /// <param name="lon">Longitude</param>
        /// <returns>Point(x,y) screen coordinate</returns>
        public Point ConvertToMapPoint(double lat, double lon)
        {
            int X = LonToX(lon);
            int CX = LonToX(centerLongitude);
            int Y = LatToY(lat);
            int CY = LatToY(centerLatitude);
            int COX = (int)Math.Round((double)(width / 2));
            int COY = (int)Math.Round((double)(height / 2));
            int DX = (int)Math.Round((double)((X - CX) >> (21 - (int)zoomLevel)));
            int DY = (int)Math.Round((double)((Y - CY) >> (21 - (int)zoomLevel)));
            return new Point(COX + DX, COY + DY);
        }
        /// <summary>
        /// Converts degrees to radian
        /// </summary>
        /// <param name="angle">Angle in degrees</param>
        /// <returns>Angle in radian</returns>
        private double DegreeToRadian(double angle)
        {
            return Math.PI * angle / 180.0;
        }
        /// <summary>
        /// Calculates the distance between the geographical points
        /// </summary>
        /// <param name="lat1">Latitude of point1</param>
        /// <param name="lon1">Longitude of point1</param>
        /// <param name="lat2">Latitude of point2</param>
        /// <param name="lon2">Longitude of point2</param>
        /// <returns>Distance in meters</returns>
        public double GetDistanceBetweenTwoCoordinates(double lat1, double lon1, double lat2, double lon2)
        {
            int R = 6371; // earth's mean radius in km
            double dLat = DegreeToRadian(lat2 - lat1);
            double dLon = DegreeToRadian(lon2 - lon1);
            lat1 = DegreeToRadian(lat1);
            lat2 = DegreeToRadian(lat2);

            double a = Math.Sin(dLat / 2.0) * Math.Sin(dLat / 2.0) + Math.Cos(lat1) * Math.Cos(lat2) * Math.Sin(dLon / 2.0) * Math.Sin(dLon / 2.0);
            double c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
            return R * c;
        }
        /// <summary>
        /// Draws a marker on the map/radar
        /// </summary>
        /// <param name="g">The Graphics object of the map/radar</param>
        /// <param name="x">X coordinate</param>
        /// <param name="y">Y coordinate</param>
        /// <param name="text">Marker text</param>
        /// <param name="actual">Is marker shows up-to-date position?</param>
        private void DrawMarker(ref Graphics g, int x, int y, string text, bool actual)
        {
            SizeF stringSize = new SizeF();
            stringSize = g.MeasureString(text, fontMarker);
            int stringWidth = Convert.ToInt32(stringSize.Width);
            int stringHeight = Convert.ToInt32(stringSize.Height);
            Point[] markerPoints = {
                                       new Point(x, y),
                                       new Point(x + 10, y - stringHeight - 5),
                                       new Point(x + 10 + stringWidth + 5, y - stringHeight - 5),
                                       new Point(x + 10 + stringWidth + 5, y - 5),
                                       new Point(x + 10, y - 5)
                                   };
            g.FillEllipse(brushMarker, x - 5, y - 5, 10, 10);
            g.FillPolygon(brushMarker, markerPoints);
            g.DrawPolygon(penMarker, markerPoints);
            if (actual)
            {
                g.FillEllipse(brushActive, new Rectangle(x - 3, y - 3, 6, 6));
            }
            else
            {
                g.FillEllipse(brushInactive, new Rectangle(x - 3, y - 3, 6, 6));
            }
            g.DrawString(text, fontMarker, brushMarkerFont, x + 12, y - stringHeight - 5);
        }
        /// <summary>
        /// Redraws all the markers on the map
        /// </summary>
        /// <param name="g">The Graphics object of the map</param>
        /// <param name="contacts">The Contacts object with the contact list</param>
        public void UpdateMarkers(ref Graphics g, Contacts contacts)
        {
            foreach (Contacts.Buddy buddy in contacts.contactList)
            {
                if (buddy.pos != null)
                {
                    Point point = ConvertToMapPoint(buddy.pos.latitude, buddy.pos.longitude);
                    DrawMarker(ref g, point.X, point.Y, buddy.name.ToString(), (buddy.lastSeen.AddHours(1) > DateTime.Now));
                }
            }
        }
        /// <summary>
        /// Set the center of the map to the average position of all the contacts
        /// </summary>
        /// <param name="contacts">The Contacts object with the contact list</param>
        public void SetCenterToAverage(Contacts contacts)
        {
            double sumlat = 0;
            double sumlon = 0;
            int count = 0;
            foreach (Contacts.Buddy buddy in contacts.contactList)
            {
                if (buddy.pos != null)
                {
                    sumlat += buddy.pos.latitude;
                    sumlon += buddy.pos.longitude;
                    count += 1;
                }
            }
            centerLatitude = sumlat / count;
            centerLongitude = sumlon / count;
        }
        /// <summary>
        /// Fit the map center and zoom to contact locations
        /// </summary>
        /// <param name="contacts">The Contacts object with the contact list</param>
        public void FitToContacts(Contacts contacts)
        {
            SetCenterToAverage(contacts);
            // Determine min/max lat/lon to select zoom level
            double minlat = double.MaxValue, minlon = double.MaxValue, maxlat = double.MinValue, maxlon = double.MinValue;
            foreach (Contacts.Buddy buddy in contacts.contactList)
            {
                if (buddy.pos.latitude < minlat) minlat = buddy.pos.latitude;
                else if (buddy.pos.latitude > maxlat) maxlat = buddy.pos.latitude;
                if (buddy.pos.longitude < minlon) minlon = buddy.pos.longitude;
                else if (buddy.pos.longitude > maxlon) maxlon = buddy.pos.longitude;
            }
            // Determine the max distance
            double maxDistance = GetDistanceBetweenTwoCoordinates(minlat, minlon, maxlat, maxlon);
            if (maxDistance < 2.68) zoomLevel = 14;
            else if (maxDistance < 5.36) zoomLevel = 13;
            else if (maxDistance < 10.72) zoomLevel = 12;
            else if (maxDistance < 21.44) zoomLevel = 11;
            else if (maxDistance < 85.76) zoomLevel = 9;
            else if (maxDistance < 343.04) zoomLevel = 7;
            else if (maxDistance < 1372.16) zoomLevel = 5;
            else if (maxDistance < 5488.64) zoomLevel = 3;
            else zoomLevel = 1;
        }
        /// <summary>
        /// Begins to download the map with the corresponding parameters
        /// </summary>
        /// <param name="settings">The Settings object with the user identifiers</param>
        public void DownloadGoogleMap(ref Settings settings)
        {
            if (zoomLevel == 1)
            {
                centerLatitude = 0;
                centerLongitude = 0;
            }

            string url = "http://maps.google.com/staticmap?";
            url += "size=" + width.ToString() + "x" + height.ToString();
            url += "&maptype=" + settings.GoogleMapType;
            url += "&center=" + centerLatitude.ToString("F6", googleCultureInfo) + "," + centerLongitude.ToString("F6", googleCultureInfo);
            url += "&zoom=" + zoomLevel.ToString();
            url += "&key=" + settings.GoogleApiKey;

            text = centerLatitude.ToString("F6", googleCultureInfo) + "\r\n" + centerLongitude.ToString("F6", googleCultureInfo) + "\r\n" + zoomLevel.ToString() + "\r\n" + state.ToString();
            countGoogleRequests++;
            try
            {
                updateMessageCallback("Downloading Google map...");
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
                request.Credentials = CredentialCache.DefaultCredentials;
                request.Timeout = 10000;

                curMap = noImageBitmap;

                Communicator.RequestState requestState = new Communicator.RequestState(request, new object(), url);
                IAsyncResult result = request.BeginGetResponse(new AsyncCallback(DownloadGoogleMapCallback), requestState);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                updateMessageCallback("Failed to load map.");
            }
        }
        /// <summary>
        /// Handles the event when the map download completes
        /// Refreshes map and modifies the GUI
        /// </summary>
        /// <param name="result">Event result</param>
        public void DownloadGoogleMapCallback(IAsyncResult result)
        {
            try
            {
                Communicator.RequestState requestState = (Communicator.RequestState)result.AsyncState;
                WebRequest request = (WebRequest)requestState.Request;
                HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result);
                System.IO.Stream bitmapStream = (System.IO.Stream)response.GetResponseStream();

                curMap = new System.Drawing.Bitmap(bitmapStream);
                mapRefreshCallback();
                updateMessageCallback("Google map updated.");
                bitmapStream.Close();
                response.Close();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                updateMessageCallback("Failed to load map.");
            }
        }
        /// <summary>
        /// Converts a geographical point the radar coordinates
        /// </summary>
        /// <param name="lat">Latitude of the point</param>
        /// <param name="lon">Longitude of the point</param>
        /// <param name="my_lat">Latitude of the user</param>
        /// <param name="my_lon">Longitude of the user</param>
        /// <returns>Point object the contains the coordinates</returns>
        private Point LatLonToRadar(double lat, double lon, double my_lat, double my_lon)
        {
            // Normalize coordinate difference to [-1..1] interval
            double normDLon = (LonToX(lon - my_lon) / Convert.ToDouble(offset)) - 1;
            double normDLat = (LatToY(lat - my_lat) / Convert.ToDouble(offset)) - 1;
            // Project [-1..1] interval to log scaled pixel coords
            int X = Convert.ToInt32(Math.Log(Math.Abs(normDLon * 100 * zoomRadar) + 1) * radarXNormalizer);
            int Y = Convert.ToInt32(Math.Log(Math.Abs(normDLat * 100 * zoomRadar) + 1) * radarYNormalizer);
            // Polarity correction
            if (normDLon < 0) X = X * -1;
            if (normDLat < 0) Y = Y * -1;
            return new Point(X + width/2, Y + height/2);
        }
        /// <summary>
        /// Changes the state to radar mode
        /// </summary>
        public void SwitchToRadar()
        {
            System.IO.Stream dataStream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("GeoPal.Resources.radar.jpg");
            curMap = new System.Drawing.Bitmap(dataStream);
            state = State.Radar;
            text = "Radar";
        }
        /// <summary>
        /// Redraws the radar and all the markers on it
        /// </summary>
        /// <param name="g">The Graphics object of the radar</param>
        /// <param name="contacts">The Contacts object with the contact list</param>
        /// <param name="position">The Position object with the current position</param>
        public void UpdateRadar(ref Graphics g, Contacts contacts, Gps.Position position)
        {
            Pen circlePen = new Pen(Color.Red);
            Brush textBrush = new SolidBrush(Color.Red);
            Font font = new Font(FontFamily.GenericMonospace, 6, FontStyle.Bold);
            foreach (Contacts.Buddy buddy in contacts.contactList)
            {
                if (buddy.pos != null)
                {
                    Point point = LatLonToRadar(buddy.pos.latitude, buddy.pos.longitude, position.latitude, position.longitude);
                    DrawMarker(ref g, point.X, point.Y, buddy.name.ToString(), (buddy.lastSeen.AddHours(1) > DateTime.Now));
                }
            }
        }
        /// <summary>
        /// Initializes the splash screen
        /// </summary>
        public void LoadSplashScreen()
        {
            System.Reflection.Assembly thisExe;
            thisExe = System.Reflection.Assembly.GetExecutingAssembly();
            System.IO.Stream dataStream = thisExe.GetManifestResourceStream("GeoPal.Resources.splash.jpg");
            curMap = new System.Drawing.Bitmap(dataStream);
        }
        /// <summary>
        /// Set the center of the map to the given screen coordinates
        /// </summary>
        /// <param name="x">X coordinate</param>
        /// <param name="y">Y coordinate</param>
        public void SetCenter(int x, int y)
        {
            int DX = ((x - width/2) << (21 - (int)zoomLevel));
            int DY = ((y - height/2) << (21 - (int)zoomLevel));

            centerLongitude = XToLon(LonToX(centerLongitude) + DX);
            centerLatitude = YToLat(LatToY(centerLatitude) + DY);
        }
        /// <summary>
        /// Increases the zoom level
        /// </summary>
        /// <returns>True, if it was successful. Otherwise, False.</returns>
        public bool ZoomIn()
        {
            if(state == State.GoogleMap)
            {
                if (zoomLevel < 15) zoomLevel++;
                else return false;
            }
            else if(state == State.Radar)
            {
                if (zoomRadar < 100) zoomRadar++;
                else return false;
            }
            return true;
        }
        /// <summary>
        /// Decreases the zoom level
        /// </summary>
        /// <returns>True, if it was successful. Otherwise, False.</returns>
        public bool ZoomOut()
        {
            if(state == State.GoogleMap)
            {
                if (zoomLevel > 1) zoomLevel--;
                else return false;
            }
            else if (state == State.Radar)
            {
                if (zoomRadar > 1) zoomRadar--;
                else return false;
            }
            return true;
        }
    }
}
